from utils.copy import smartcopy
from utils.interval import Interval

from cranesched.routing.movable1d import Movable1D


class Movable2D(object):
    """This class implements the "movable" part of a crane, i.e. an object with a 2D position and 
    (x, y) size and maximum speed, that can be moved around the two-dimensional Euclidean space. 
    Movement along the two axes is independent, but the crane can move freely along the y-axis, 
    while the path along the x-axis must be planned by means of a Router object in order to avoid 
    collisions with other cranes.
    """
    UNREACHABLE_POSITION = "crane is unable to reach target position"
    INSUFFICIENT_TIME = "unable to complete assignment before (hard) end of window"
    
    def __init__(self, id, time=0.0, 
                 x=0.0, xspeed=1.0, xsize=1.0, 
                 y=0.0, yspeed=1.0, ysize=1.0):
        self.x = Movable1D("%s.x" % id, time, x, xspeed, xsize)
        self.y = Movable1D("%s.y" % id, time, y, yspeed, ysize)
        self.traveled_distance = 0.0
        self.path_state_stack = []
        self.router = None
        
    def __deepcopy__(self, memo):
        clone = smartcopy(self, memo, 
                          shallowcopy_attrs=("traveled_distance",), 
                          deepcopy_attrs=("x", "y", "path_state_stack"))
        clone.router = None
        return clone
        
    @property
    def time(self):
        return self.x.path.time
        
    @property
    def position(self):
        return (self.x.path.position, self.y.path.position)
        
    def set_initial_position(self, time=0.0, x=0.0, y=0.0):
        self.x.set_initial_position(time, x)
        self.y.set_initial_position(time, y)
        
    def set_router(self, router):
        """Set the crane's Router object. This adds the crane's x-axis movable to the router."""
        if self.router is not None:
            raise Exception("router is already set")
        self.router = router
        self.router.add(self.x)
        
    def can_fulfill(self, assignment):
        return self._can_fulfill(assignment) is None
        
    def _can_fulfill(self, assignment):
        """Returns None if the crane is able to fulfill the request before the end of its 
        satisfiability window (if the window end constraint is hard). If the assignment cannot be 
        fulfilled, an error string containing the reason for the failure is returned."""
        # first of all check if the x-coordinate is reachable by the crane
        activity = assignment.activity
        if not self.router.is_reachable(self.x, activity.x):
            return Movable2D.UNREACHABLE_POSITION
        # since we can move freely along the y-axis, we first compute when that movement is  
        # finished, and use this result to adjust (if necessary) the assignment's satisfiability 
        # window (remember that this is passed on to the router as an interval)
        window = assignment.window
        y_move_duration = self.y.time_to(activity.y)
        y_move_finished = self.time + y_move_duration
        if y_move_finished + activity.duration > window.hard_end:
            return Movable2D.INSUFFICIENT_TIME
        if y_move_finished > window.start:
            window.start = y_move_finished
        # ask the router to determine the earliest date at which the crane can be at the target 
        # coordinates for the requested duration within the given time interval. If None is 
        # returned, it means the crane cannot reach (and stay at) the desired position within the 
        # argument time window
        interval = Interval(window.start, window.hard_end)
        earliest_fit = self.router.earliest_fit(self.x, activity.x, activity.duration, interval)
        if earliest_fit is None:
            return Movable2D.INSUFFICIENT_TIME
        x_trajectory = self.router.compute_trajectory(self.x, activity.x, earliest_fit)
        y_trajectory = [(y_move_finished, activity.y)] if y_move_duration > 0.0 else []
        assignment.set_move(x_trajectory, y_trajectory)
        #assignment.set_fulfillment(self.time, earliest_fit + activity.duration)
        return None
        
    def fulfill(self, assignment):
        """Fulfill a given assignment, i.e. move the crane into the target coordinates by planning 
        a collision-free route. Note that can_fulfill() must be called previously to set some 
        necessary attributes on the assignment object, like the date of the earliest fit of the 
        assignment."""
        # save current crane state (size of x- and y-paths, and traveled distance is enough)
        self.path_state_stack.append((len(self.x.path), len(self.y.path), self.traveled_distance))
        # move into the target position
        self.x.path.extend(assignment.x_trajectory)
        self.y.path.extend(assignment.y_trajectory)
        self.traveled_distance += assignment.traveled_distance
        # save previous bounds and recompute bounds with new x path
        self.router.push()
        self.router.update()
        
    def undo(self):
        """Undo the last assignment done by this crane. This undoes any position changes done by 
        fulfill()."""
        # rewind x- and y-path to previous state
        i, j, self.traveled_distance = self.path_state_stack.pop()
        for k, path in [(i, self.x.path), (j, self.y.path)]:
            while len(path) > k:
                path.pop()
        # restore previous movement bounds at the router
        self.router.pop()
        
    # def move_y(self, assignment):
    #     """Movement along the y-axis can be done immediately since there are no constraints on 
    #     this axis other than the warehouse's limits."""
    #     target_y = assignment.activity.y
    #     if self.y.position != target_y:
    #         self.traveled_distance += abs(target_y - self.y.position)
    #         y_move_finished = self.time + self.y.time_to(target_y)
    #         self.y.path.append(y_move_finished, target_y)
    #     self.y.path.append(assignment.exec_end)
        
    # def move_x(self, assignment):
    #     """Use the router to plan our path along the x-axis without colliding into other cranes."""
    #     target_x = 
        
    #     x0 = self.x.position
    #     for _, x1 in trajectory:
    #         self.traveled_distance += abs(x1 - x0)
    #         x0 = x1
    #     self.x.path.extend(assignment.xtrajectory)
    #     self.x.path.append(assignment.exec_end)
        
